xtask\tasks\fuzz/
html_coverage.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Glue to generate HTML LCOV-based coverage reports from `cargo-fuzz`
5//! `coverage.profdata` files.
6
7use anyhow::Context;
8use std::path::Path;
9use std::path::PathBuf;
10
11pub(super) fn generate_html_coverage_report(
12    ctx: &crate::XtaskCtx,
13    target_fuzz_dir: &Path,
14    target_name: &str,
15) -> Result<(), anyhow::Error> {
16    // cargo-fuzz will always dump the data here
17    let coverage_profdata_file = target_fuzz_dir
18        .join("coverage")
19        .join(target_name)
20        .join("coverage.profdata");
21
22    // would be great if there was an easy way to find where the resulting
23    // `cargo fuzz coverage` bin gets dumped to... but this seems to work ok.
24    let coverage_bin = {
25        let mut coverage_bin: Option<PathBuf> = None;
26        for e in walkdir::WalkDir::new(ctx.root.join("target")) {
27            let e = e?;
28            if e.file_name() != target_name
29                || !e.path().components().any(|c| c.as_os_str() == "coverage")
30            {
31                continue;
32            }
33
34            // instead of immediately breaking, as a sanity check, keep looking
35            // for other matches, erroring out if there is a dupe (which would
36            // indicate that this logic needs some more tweaking)
37            if let Some(existing_bin) = &coverage_bin {
38                panic!(
39                    "xtask bug: found multiple potential coverage bins: {} and {}",
40                    existing_bin.display(),
41                    e.path().display()
42                )
43            } else {
44                coverage_bin = Some(e.into_path());
45            }
46        }
47        coverage_bin.expect("xtask bug: failed to find the coverage-instrumented fuzzer bin")
48    };
49
50    let llvm_tools_dir = 'llvm_tools_dir: {
51        let output = std::process::Command::new("rustc")
52            .args(["+nightly", "--print", "sysroot"])
53            .output()
54            .context("failed to run `rustc +nightly --print sysroot`")?;
55        let rustc_sysroot = String::from_utf8_lossy(&output.stdout).to_string();
56        let rustc_sysroot = rustc_sysroot.trim();
57
58        for e in walkdir::WalkDir::new(rustc_sysroot) {
59            let e = e?;
60            if e.file_name() == "llvm-profdata" {
61                let mut path = e.into_path();
62                path.pop();
63                break 'llvm_tools_dir path;
64            }
65        }
66
67        anyhow::bail!(
68            "failed to find `llvm-tools` directory. did you run `rustup +nightly component add llvm-tools`?"
69        )
70    };
71
72    if which::which("lcov").is_err() {
73        anyhow::bail!(
74            "could not find `lcov` on your $PATH! make sure it's installed (e.g: `apt install lcov`)"
75        )
76    }
77
78    let coverage_dir = coverage_bin.parent().unwrap();
79    let coverage_lcov_file = coverage_dir.join("coverage.lcov");
80    {
81        let lcov_output = std::fs::File::create(&coverage_lcov_file)?;
82
83        let mut cmd = std::process::Command::new(llvm_tools_dir.join("llvm-cov"));
84        let mut cmd = cmd
85            .arg("export")
86            .arg("-instr-profile")
87            .arg(coverage_profdata_file)
88            .arg("-format=lcov")
89            .arg("-object")
90            .arg(&coverage_bin)
91            .args(["--ignore-filename-regex", "rustc"])
92            .stdout(std::process::Stdio::from(lcov_output))
93            .spawn()?;
94        if !cmd.wait()?.success() {
95            anyhow::bail!("failed while running `llvm-cov`")
96        }
97    }
98
99    let html_report_dir = coverage_dir.join(format!("lcov_html_{}", target_name));
100    {
101        // summarize the coverage information
102        let mut cmd = std::process::Command::new("lcov");
103        let mut cmd = cmd.arg("--summary").arg(&coverage_lcov_file).spawn()?;
104        if !cmd.wait()?.success() {
105            anyhow::bail!("failed while running `lcov`")
106        };
107
108        // make an output directory for the html report
109        if html_report_dir.exists() {
110            fs_err::remove_dir_all(&html_report_dir)?;
111        }
112        fs_err::create_dir(&html_report_dir)?;
113
114        // generate the html report
115        let mut cmd = std::process::Command::new("genhtml");
116        let mut cmd = cmd
117            .arg("-o")
118            .arg(&html_report_dir)
119            .arg("--legend")
120            .arg("--highlight")
121            .arg(coverage_lcov_file)
122            .spawn()?;
123        if !cmd.wait()?.success() {
124            anyhow::bail!("failed while running `genhtml`")
125        }
126    }
127
128    log::info!("");
129    log::info!(
130        "success! html report generated at {}",
131        html_report_dir.join("index.html").display()
132    );
133
134    Ok(())
135}